package npr;

import processing.core.*;
import processing.opengl.*;

import java.nio.*;
import javax.media.opengl.*;
import com.sun.opengl.util.*;
import java.util.LinkedList;
import java.util.ArrayList;
import java.util.ListIterator;

import java.lang.RuntimeException;

import java.io.File;

import npr.gui.GUI;

public class NPR extends PGraphicsOpenGL{
  
    public static String renderer= "npr.NPR";

    GLSL main;
    
    protected boolean renderTriangles;

    protected ShortBuffer screen;

    protected boolean depthSort;

    protected int textureUnits[];
    protected int textureUnit= 1;

    protected int startTriangle;

    //bounding box
    PVector minBounds, maxBounds; 
    PVector[] objectBounds;
    PVector[] worldBounds;

    public PVector lightPositionWorld[], lightNormalWorld[];

    public float eyeX, eyeY, eyeZ, farPlane; 

    LinkedList<Algorithm> algorithms;
    History history;

    protected boolean backFaceCulling= true;

    int activeRenderers= 0;

    public NPR(){
	history= new History();
	algorithms= new LinkedList<Algorithm>();
        minBounds= new PVector(); 
	maxBounds= new PVector();
	objectBounds= new PVector[8];
	lightPositionWorld= new PVector[MAX_LIGHTS];
	for(int i= 0; i<MAX_LIGHTS; i++){
	    lightPositionWorld[i]= new PVector(); 
	}
	lightNormalWorld= new PVector[MAX_LIGHTS];
	for(int i= 0; i<MAX_LIGHTS; i++){
	    lightNormalWorld[i]= new PVector(); 
	}
    }


    public void setParent(PApplet parent){
	super.setParent(parent);
	GLSL.setParent(parent);
	GUI.setParent(parent);
	LookUpTable.setParent(parent);
	RenderToTexture.setParent(parent);
	Algorithm.setParent(parent);
	OBJModel.setParent(parent);
	VirtualTrackball.setParent(parent);
    }

/**
 * enables back-face culling.<br>
 * back-face culling determines whether a polygon of a graphical object is visible, depending on the position of the camera.<br> 
 It is a step in the OpenGL graphical pipeline that tests whether the points in the polygon appear in clockwise or counter-clockwise order when projected onto the screen. If the user has specified that front-facing polygons have a clockwise winding, if the polygon projected on the screen has a counter-clockwise winding it has been rotated to face away from the camera and will not be drawn,<br>
 * This process makes rendering objects quicker and more efficient by reducing the number of polygons for the program to draw
 */
    public void enableBackFaceCulling(){
	
	this.backFaceCulling= true;
    }

/**
 * disables back-face culling.<br>
 * back-face culling determines whether a polygon of a graphical object is visible, depending on the position of the camera.<br> 
 It is a step in the OpenGL graphical pipeline that tests whether the points in the polygon appear in clockwise or counter-clockwise order when projected onto the screen. If the user has specified that front-facing polygons have a clockwise winding, if the polygon projected on the screen has a counter-clockwise winding it has been rotated to face away from the camera and will not be drawn,<br>
 * This process makes rendering objects quicker and more efficient by reducing the number of polygons for the program to draw
 */
    public void disableBackFaceCulling(){
	
	this.backFaceCulling= false;
    }


/**
   * Called by resize(), this handles creating the actual GLCanvas the
   * first time around, or simply resizing it on subsequent calls.
   * There is no pixel array to allocate for an OpenGL canvas
   * because OpenGL's pixel buffer is all handled internally.
   */
  protected void allocate() {
    if (context == null) {
//      System.out.println("PGraphicsOpenGL.allocate() for " + width + " " + height);
//      new Exception().printStackTrace(System.out);
      // If OpenGL 2X or 4X smoothing is enabled, setup caps object for them
      GLCapabilities capabilities = new GLCapabilities();
      // Starting in release 0158, OpenGL smoothing is always enabled
      if (!hints[DISABLE_OPENGL_2X_SMOOTH]) {
        capabilities.setSampleBuffers(true);
        capabilities.setNumSamples(2);
      } else if (hints[ENABLE_OPENGL_4X_SMOOTH]) {
        capabilities.setSampleBuffers(true);
        capabilities.setNumSamples(4);
      }
      capabilities.setAlphaBits(8);

      // get a rendering surface and a context for this canvas
      GLDrawableFactory factory = GLDrawableFactory.getFactory();

      /*
      if (PApplet.platform == PConstants.LINUX) {
        GraphicsConfiguration pconfig = parent.getGraphicsConfiguration();
        System.out.println("parent config is " + pconfig);

        //      GraphicsDevice device = config.getDevice();
        //AbstractGraphicsDevice agd = new AbstractGraphicsDevice(device);
        //AbstractGraphicsConfiguration agc = factory.chooseGraphicsConfiguration(capabilities, null, null);

        AWTGraphicsConfiguration agc = (AWTGraphicsConfiguration)
        factory.chooseGraphicsConfiguration(capabilities, null, null);
        GraphicsConfiguration config = agc.getGraphicsConfiguration();
        System.out.println("agc config is " + config);
      }
      */

      drawable = factory.getGLDrawable(parent, capabilities, new DefaultGLCapabilitiesChooser());
      context = drawable.createContext(null);

      // need to get proper opengl context since will be needed below
      gl = context.getGL();
      // Flag defaults to be reset on the next trip into beginDraw().
      settingsInited = false;

    } else {
      // changing for 0100, need to resize rather than re-allocate
      //System.out.println("PGraphicsOpenGL.allocate() again for " + width + " " + height);
      reapplySettings();
    }
  }

    
    protected class History{


	ArrayList<Event> history;
	LinkedList<Event> algorithmHistory;

	protected class Model{
	    
	    OBJModel model;
	    float red, green, blue, alpha;

	    protected Model(OBJModel model){

		this.model= model;
		red= fillR;
		green= fillG;
		blue= fillB;
		alpha= fillA;
	    }
	}

	protected class Event{
	    
	    int triangleIndex;
	    float[] modelview, model, camera, cameraInv;
	    LinkedList<Model> models;
	    LinkedList<Algorithm> algorithms;
	    PVector[] boundingBox;
	    PVector eye;


	    public Event(PMatrix3D modelview, PMatrix3D camera, PMatrix3D cameraInv, float eyeX, float eyeY, float eyeZ){
		
		triangleIndex= triangleCount;
		
		setModelview(modelview, camera, cameraInv);

		algorithms= new LinkedList<Algorithm>();

		models= new LinkedList<Model>();

		eye= new PVector(eyeX, eyeY, eyeZ);
	    }


	    public String toString(){
 		
		return "triangleIndex " + triangleIndex + "\n"
		    + "models: " + models.size() + "\n"
		    + "algorithms " + algorithms.size();
	    }

	    
	    public void setModelview(PMatrix3D modelview, PMatrix3D camera, PMatrix3D cameraInv){
		
		this.modelview= transpose(modelview);

		PMatrix3D model= cameraInv.get();
		model.apply(modelview);
		this.model= transpose(model);

		this.camera= transpose(camera);

		this.cameraInv= transpose(cameraInv);
	    }

	    
	    public void setBoundingBox(PVector[] boundingBox){
		
		this.boundingBox= boundingBox;
	    }


	    public boolean hasModels(){
		
		return !models.isEmpty();
	    }
	    

	    public boolean hasGeometry(){
		
		return triangleIndex<triangleCount;
	    }


	    public boolean hasAlgorithms(){
		
		return !algorithms.isEmpty();
	    }
	}


	public History(){

	    history= new ArrayList<Event>();
	    algorithmHistory= new LinkedList<Event>();
	}


	public int getIndex(Event event){

	    return history.indexOf(event);
	}


	public void addModelview(){
	    
	    Event lastEvent= history.get(history.size()-1);
	    if(!lastEvent.hasModels() && !lastEvent.hasGeometry()){
		lastEvent.setModelview(modelview, camera, cameraInv);
	    }
	    else{
		history.add(new Event(modelview, camera, cameraInv,
				      eyeX, eyeY, eyeZ));
		lastEvent.setBoundingBox(worldBounds);
	    }
	}


	public void addGeometry(){
	    
	    Event lastEvent= history.get(history.size()-1);
	    if(lastEvent.hasModels()){
		history.add(new Event(modelview, camera, cameraInv,
				      eyeX, eyeY, eyeZ));
		lastEvent.setBoundingBox(worldBounds);
	    }
	}


	public void addModel(OBJModel model){
	    
	    Event lastEvent= history.get(history.size()-1);
	    if(lastEvent.hasGeometry()){
		Event newEvent= new Event(modelview, camera, cameraInv,
				      eyeX, eyeY, eyeZ);
		newEvent.models.add(new Model(model));
		history.add(newEvent);
		lastEvent.setBoundingBox(worldBounds);
	    }
	    else{
		lastEvent.models.add(new Model(model));
	    }
	}

	
	public void addAlgorithm(Algorithm algorithm){

	    if(history.isEmpty()){
		Event newEvent= new Event(modelview, camera, cameraInv,
					  eyeX, eyeY, eyeZ);
		newEvent.algorithms.add(algorithm);
		history.add(newEvent);
		algorithmHistory.add(newEvent);
	    }
	    else{
		Event lastEvent= history.get(history.size()-1);
		Event lastAlgorithmEvent= algorithmHistory.getLast();
		if(!lastEvent.hasModels() && !lastEvent.hasGeometry()){
		    if(lastEvent==lastAlgorithmEvent){
			lastEvent.algorithms.add(algorithm);
		    }
		    else{
			lastEvent.algorithms= new LinkedList<Algorithm>(lastAlgorithmEvent.algorithms);
			lastEvent.algorithms.add(algorithm);
			algorithmHistory.add(lastEvent);
		    }
		}
		else{
		    calculateWorldBounds();
		    lastEvent.setBoundingBox(worldBounds);

		    Event newEvent= new Event(modelview, camera, cameraInv,
					      eyeX, eyeY, eyeZ);
		    newEvent.algorithms= new LinkedList<Algorithm>(lastAlgorithmEvent.algorithms);
		    newEvent.algorithms.add(algorithm);
		    history.add(newEvent);
		    algorithmHistory.add(newEvent);
		}
	    }
	    algorithm.start= history.size()-1;
	}
	

	public void removeAlgorithm(Algorithm algorithm){

	    Event lastEvent= history.get(history.size()-1);
	    Event lastAlgorithmEvent= algorithmHistory.getLast();
	    if(!lastEvent.hasModels() && !lastEvent.hasGeometry()){
		if(lastEvent==lastAlgorithmEvent){
		    lastEvent.algorithms.remove(algorithm);
		}
		else{
		    lastEvent.algorithms= new LinkedList<Algorithm>(lastAlgorithmEvent.algorithms);
		    lastEvent.algorithms.remove(algorithm);
		    algorithmHistory.add(lastEvent);
		    }
	    }
	    else{
		calculateWorldBounds();
		lastEvent.setBoundingBox(worldBounds);

		Event newEvent= new Event(modelview, camera, cameraInv,
					  eyeX, eyeY, eyeZ);
		newEvent.algorithms= new LinkedList<Algorithm>(lastAlgorithmEvent.algorithms);
		newEvent.algorithms.remove(algorithm);
		history.add(newEvent);
		algorithmHistory.add(newEvent);
	    }
	    algorithm.stop= history.size()-1;
	}

	
	protected Event get(int i){

	    return history.get(i);
	}


	protected int length(){

	    return history.size();
	}


	protected void clear(){

	    history.clear();
	    algorithmHistory.clear();
	}
    }

    
    protected void registerModel(OBJModel model){

	history.addModel(model);
	if(model.minBounds.x<this.minBounds.x) this.minBounds.x= model.minBounds.x;
	if(model.maxBounds.x>this.maxBounds.x) this.maxBounds.x= model.maxBounds.x;
	if(model.minBounds.y<this.minBounds.y) this.minBounds.y= model.minBounds.y;
	if(model.maxBounds.y>this.maxBounds.y) this.maxBounds.y= model.maxBounds.y;
	if(model.minBounds.z<this.minBounds.z) this.minBounds.z= model.minBounds.z;
	if(model.maxBounds.z>this.maxBounds.z) this.maxBounds.z= model.maxBounds.z;
    }


    private void calculateWorldBounds(){
	
	// We have min and max values, use these to get the 8 corners of the oriented bounding box in object space
	objectBounds[0] = new PVector(minBounds.x, minBounds.y, minBounds.z); // xyz
	objectBounds[1] = new PVector(maxBounds.x, minBounds.y, minBounds.z); // Xyz
	objectBounds[2] = new PVector(maxBounds.x, maxBounds.y, minBounds.z); // XYz
	objectBounds[3] = new PVector(minBounds.x, maxBounds.y, minBounds.z); // xYz
	objectBounds[4] = new PVector(minBounds.x, minBounds.y, maxBounds.z); // xyZ
	objectBounds[5] = new PVector(maxBounds.x, minBounds.y, maxBounds.z); // XyZ
	objectBounds[6] = new PVector(maxBounds.x, maxBounds.y, maxBounds.z); // XYZ
	objectBounds[7] = new PVector(minBounds.x, maxBounds.y, maxBounds.z); // xYZ

	// reset min and max bounds
	minBounds.x= minBounds.y= minBounds.z= maxBounds.x= maxBounds.y= maxBounds.z= 0.0f;

	// this matrix transforms points into world space
	PMatrix3D model= cameraInv.get();
	model.apply(modelview);

	worldBounds= new PVector[8];
	
	for(int i=0; i<8; i++){
	    worldBounds[i]= new PVector();
	    model.mult(objectBounds[i], worldBounds[i]);
	}
    }

    /**
     * returns the oriented bounding box in world space associated with an index in the matrix history
     * @param index index to the matrix history
     * @return oriented bounding box in world space associated with an index in the matrix history
     */

    public PVector[] getBoundingBox(int index){
	return history.get(index).boundingBox;
    }

    public float[] getModelview(int index){
	return history.get(index).modelview;
    }

    public float[] getModel(int index){
	return history.get(index).model;
    }

    public float[] getCamera(int index){
	return history.get(index).camera;
    }

    public float[] getCameraInv(int index){
	return history.get(index).cameraInv;
    }
    
    public PVector getEye(int index){
	return history.get(index).eye;
    }

    public LinkedList<OBJModel> getModels(int index){
	LinkedList<OBJModel> models= new LinkedList<OBJModel>();
	ListIterator<History.Model> listIterator= history.get(index).models.listIterator();
	while(listIterator.hasNext()){
	    models.add(listIterator.next().model);
	}
	return models;
    }

    /**
     * transposes a given PMatrix
     * @param matrix matrix to transpose
     * @return transposed matrix as a float array, which can subsequently be used in a glPushMatrix() call
     */
    protected float[] transpose(PMatrix3D matrix){
        return new float[] {
            matrix.m00, matrix.m10, matrix.m20, matrix.m30,
            matrix.m01, matrix.m11, matrix.m21, matrix.m31,
            matrix.m02, matrix.m12, matrix.m22, matrix.m32,
            matrix.m03, matrix.m13, matrix.m23, matrix.m33
        };
    }

    /**
     * transposes a given PMatrix
     * @param matrix matrix to transpose
     * @param transposed the target float vector, must be of size 16 (if null or not size 16, a new array will be created) 
     * @return transposed matrix as a float array, which can subsequently be used in a glPushMatrix() call
     */
    protected float[] transpose(PMatrix3D matrix, float transposed[]){
        
        if (transposed==null || transposed.length<16) 
            return transpose(matrix);
        
        transposed[0] = matrix.m00;
        transposed[1] = matrix.m10;
        transposed[2] = matrix.m20;
        transposed[3] = matrix.m30;

        transposed[4] = matrix.m01;
        transposed[5] = matrix.m11;
        transposed[6] = matrix.m21;
        transposed[7] = matrix.m31;
              
        transposed[8] = matrix.m02;
        transposed[9] = matrix.m12;
        transposed[10] = matrix.m22;
        transposed[11] = matrix.m32;

        transposed[12] = matrix.m03;
        transposed[13] = matrix.m13;
        transposed[14] = matrix.m23;
        transposed[15] = matrix.m33;

        return transposed;
    }
    

    /**
     * determines whether in NPR mode (between a begin()/end() block or not
     * @return in NPR mode
     */
    protected boolean npr(){
        return activeRenderers>0;
    }

    public void beginDraw() {
	super.beginDraw();
	gl.glEnable (GL.GL_DEPTH_TEST);   
	gl.glDepthMask(true);
	gl.glClear(GL.GL_DEPTH_BUFFER_BIT);
	if(hints[DISABLE_DEPTH_TEST]) gl.glDisable (GL.GL_DEPTH_TEST);
    }

    /**
     * workaround for a Processing bug: according to OpenGL specification, lightSpotAngle should be specified in degrees
     */
    public void spotLight(float r, float g, float b,
                        float x, float y, float z,
                        float nx, float ny, float nz,
                        float angle, float concentration) {
	super.spotLight(r, g, b, x, y, z, nx, ny, nz, angle, concentration);
	// bug in processing, according to OpenGL specification, the angle should be specified in degrees
	
	gl.glLightf(GL.GL_LIGHT0 + lightCount-1, GL.GL_SPOT_CUTOFF, parent.degrees(lightSpotAngle[lightCount-1]));
    }

    /**
     * calulates the light position in world space (needed by shadow mapping, which renders the scene from the light's point of view)
     */
    protected void lightPosition(int num, float x, float y, float z){
	super.lightPosition(num, x, y, z);
	PMatrix3D model= cameraInv.get();
	model.apply(modelview);
	model.mult(new PVector(x, y, z), lightPositionWorld[num]);
    }

    /**
     * calulates the light direction in world space (needed by shadow mapping, which renders the scene from the light's point of view)
     */
    protected void lightDirection(int num, float x, float y, float z){
      super.lightDirection(num, x, y, z);
      PMatrix3D modelInv= cameraInv.get();
      modelInv.apply(modelview);
      modelInv.invert();
      lightNormalWorld[num].set(modelInv.m00*x + modelInv.m10*y + modelInv.m20*z + modelInv.m30,
				modelInv.m01*x + modelInv.m11*y + modelInv.m21*z + modelInv.m31,
				modelInv.m02*x + modelInv.m12*y + modelInv.m22*z + modelInv.m32);
      lightNormalWorld[num].normalize();
  }


  public void beginShape(int kind) {
      startTriangle= triangleCount;
      super.beginShape(kind);
  }

    /**
     * overrides lighting calulation when being in NPR rendering mode (between a begin()/end() block) since lighting is done in hardware<br>
     * does calculate per-face normals
     */

    protected void endShapeLighting(boolean lights) {
        if(npr()){ // only calculate normals
            if (shape != 0) {
		if(normalMode==NORMAL_MODE_SHAPE){
		    System.out.println("normal mode shape");
		    for (int vidx = shapeFirst+1; vidx < shapeLast; vidx++) {
			vertices[vidx][NX] = vertices[shapeFirst][NX];
			vertices[vidx][NY] = vertices[shapeFirst][NY];
			vertices[vidx][NZ] = vertices[shapeFirst][NZ];
		    }
		}
                else if (normalMode == NORMAL_MODE_AUTO) {
                    //System.out.println("normal mode auto");
                    for (int triIndex = startTriangle; triIndex < triangleCount; triIndex++) {
                        int vIndex1 = triangles[triIndex][VERTEX1];
                        int vIndex2 = triangles[triIndex][VERTEX2];
                        int vIndex3 = triangles[triIndex][VERTEX3];
                        
                        cross(vertices[vIndex2][VX] - vertices[vIndex1][VX],
                              vertices[vIndex2][VY] - vertices[vIndex1][VY],
                              vertices[vIndex2][VZ] - vertices[vIndex1][VZ],
                              vertices[vIndex3][VX] - vertices[vIndex1][VX],
                              vertices[vIndex3][VY] - vertices[vIndex1][VY],
                              vertices[vIndex3][VZ] - vertices[vIndex1][VZ],
                              lightTriangleNorm);
                        
                        lightTriangleNorm.normalize();
                        vertices[vIndex1][NX] = -lightTriangleNorm.x;
                        vertices[vIndex1][NY] = -lightTriangleNorm.y;
                        vertices[vIndex1][NZ] = -lightTriangleNorm.z;
                        
                        vertices[vIndex2][NX] = -lightTriangleNorm.x;
                        vertices[vIndex2][NY] = -lightTriangleNorm.y;
                        vertices[vIndex2][NZ] = -lightTriangleNorm.z;
                        
                        vertices[vIndex3][NX] = -lightTriangleNorm.x;
                        vertices[vIndex3][NY] = -lightTriangleNorm.y;
                        vertices[vIndex3][NZ] = -lightTriangleNorm.z;
                    }
                }   
            }
        }
        super.endShapeLighting(lights && !npr());
    }

    private final void cross(float a0, float a1, float a2,
                             float b0, float b1, float b2, 
                             PVector out) {
        out.x = a1*b2 - a2*b1;
        out.y = a2*b0 - a0*b2;
        out.z = a0*b1 - a1*b0;
    }
   
    /**
     * since all transformations are done in hardware,just fill in the object space coordinates.<br> 
     * in addition, the axis-aligned bounding box caluclation (in object space) is done
     */
    protected void endShapeModelToCamera(int start, int stop) {
        if(npr()){ 
            for (int i = start; i < stop; i++) {
                float vertex[] = vertices[i];
                
		vertex[VX]= vertex[X];
		vertex[VY]= vertex[Y];
		vertex[VZ]= vertex[Z];

		if(minBounds.x==0.0f &&
		   minBounds.y==0.0f &&
		   minBounds.z==0.0f &&
		   maxBounds.x==0.0f &&
		   maxBounds.y==0.0f &&
		   maxBounds.z==0.0f){
		 
		    // initialize bounding box

		    minBounds.x= maxBounds.x= vertex[VX];
		    minBounds.y= maxBounds.y= vertex[VY];
		    minBounds.z= maxBounds.z= vertex[VZ];
		}
		else{
		    // calculate bounding box
		    
		    if(vertex[VX]<minBounds.x) minBounds.x= vertex[VX];
		    if(vertex[VX]>maxBounds.x) maxBounds.x= vertex[VX];
                
		    if(vertex[VY]<minBounds.y) minBounds.y= vertex[VY];
		    if(vertex[VY]>maxBounds.y) maxBounds.y= vertex[VY];
		    
		    if(vertex[VZ]<minBounds.z) minBounds.z= vertex[VZ];
		    if(vertex[VZ]>maxBounds.z) maxBounds.z= vertex[VZ];
                
		vertex[VW]= 1.0f;                
		}
            }
        }   
        else{
            super.endShapeModelToCamera(start, stop);
        }
    }


// TODO not the least bit efficient, it even redraws lines
  // along the vertices. ugly ugly ugly!
  public void box(float w, float h, float d) {
    
      if(npr()){
	  float x1 = -w/2f; float x2 = w/2f;
	  float y1 = -h/2f; float y2 = h/2f;
	  float z1 = -d/2f; float z2 = d/2f;
	  
	  beginShape(QUADS);

	  // front
	  vertex(x1, y1, z1);
	  vertex(x2, y1, z1);
	  vertex(x2, y2, z1);
	  vertex(x1, y2, z1);
	  
	  // right
	  vertex(x2, y1, z1);
	  vertex(x2, y1, z2);
	  vertex(x2, y2, z2);
	  vertex(x2, y2, z1);
	  
	  // back
	  vertex(x2, y1, z2);
	  vertex(x1, y1, z2);
	  vertex(x1, y2, z2);
	  vertex(x2, y2, z2);
	  
	  // left
	  vertex(x1, y1, z2);
	  vertex(x1, y1, z1);
	  vertex(x1, y2, z1);
	  vertex(x1, y2, z2);

	  // top
	  vertex(x1, y1, z2);
	  vertex(x2, y1, z2);
	  vertex(x2, y1, z1);
	  vertex(x1, y1, z1);

	  // bottom
	  vertex(x1, y2, z1);
	  vertex(x2, y2, z1);
	  vertex(x2, y2, z2);
	  vertex(x1, y2, z2);

	  endShape();
      }
      else super.box(w, h, d);
  }


    //////////////////////////////////////////////////////////////

    // MATRIX TRANSFORMATIONS

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void translate(float tx, float ty, float tz) {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.translate(tx, ty, tz);
        if(npr() && !manipulatingCamera) history.addModelview();
    }
    
    /**
     * calculates world space bounding box, updates matrix history
     */
    public void rotateX(float angle) {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.rotateX(angle);
        if(npr() && !manipulatingCamera) history.addModelview();
    }

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void rotateY(float angle) {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.rotateY(angle);
        if(npr() && !manipulatingCamera) history.addModelview();
    }

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void rotateZ(float angle) {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.rotateZ(angle);
        if(npr() && !manipulatingCamera) history.addModelview();
    }

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void rotate(float angle, float v0, float v1, float v2) {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.rotate(angle, v0, v1, v2);
        if(npr() && !manipulatingCamera) history.addModelview();
    }

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void scale(float x, float y, float z) {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.scale(x, y, z);
        if(npr() && !manipulatingCamera) history.addModelview();
    }
    
    /**
     * calculates world space bounding box, updates matrix history
     */
    public void resetMatrix() {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.resetMatrix();
        if(npr() && !manipulatingCamera) history.addModelview();
    }

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void applyMatrix(float n00, float n01, float n02, float n03,
                            float n10, float n11, float n12, float n13,
                            float n20, float n21, float n22, float n23,
                            float n30, float n31, float n32, float n33) {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();

        super.applyMatrix(n00, n01, n02, n03,
                          n10, n11, n12, n13,
                          n20, n21, n22, n23,
                          n30, n31, n32, n33);

        if(npr() && !manipulatingCamera) history.addModelview();
    }


    /* modelview matrix does not change here
       public void pushMatrix() {
       super.pushMatrix();
       updateMatrixHistory();
       }
    */

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void popMatrix() {
	
	if(npr() && !manipulatingCamera) calculateWorldBounds();
        super.popMatrix();
	if(npr() && !manipulatingCamera) history.addModelview();
    }
   
    /**
     * calculates world space bounding box, updates matrix history
     */
    public void camera(float eyeX, float eyeY, float eyeZ,
                     float centerX, float centerY, float centerZ,
                     float upX, float upY, float upZ) {
	
	this.eyeX= eyeX;
	this.eyeY= eyeY;
	this.eyeZ= eyeZ;
	if(npr()) calculateWorldBounds();
	super.camera(eyeX, eyeY, eyeZ,
                   centerX, centerY, centerZ,
                   upX, upY, upZ);
	if(npr()) history.addModelview();
    }

    /**
     * calculates world space bounding box, updates matrix history
     */
    public void endCamera(){
	
	if(npr()) calculateWorldBounds();
	super.endCamera();
	if(npr()) history.addModelview();
    }
       
    /**
     * remembers far plane (needed for shadow mapping 
     */
    public void frustum(float left, float right, float bottom,
                      float top, float znear, float zfar) {
	this.farPlane= zfar;
	super.frustum(left, right, bottom, top, znear, zfar);
    }

    /**
     * registers an algorithm with the NPR renderer.
     * sets all necessary state for NPR rendering
     */
    protected void begin(Algorithm algorithm){
	renderTriangles= true;
	// can't do this in contructor because gl is not initialized at that point
	if(main==null){
	    main=new GLSL();
	    main.loadVertexShader("main-shader.vert");
	    main.loadFragmentShader("main-shader.frag");
	    main.useShaders();
	}
	// can't do this in contructor because gl is not initialized at that point
	if(textureUnits==null){
	    int[] maxTextureUnits = new int[1];
	    gl.glGetIntegerv(GL.GL_MAX_TEXTURE_UNITS, maxTextureUnits, 0);
	    int availableTextureUnits = maxTextureUnits[0];
	    textureUnits= new int[availableTextureUnits];
	    for(int i=0; i<availableTextureUnits; i++){
		textureUnits[i]= GL.GL_TEXTURE0+i;
	    }
	}

	if(!npr()){
            flush();
            if(!fill){
		renderTriangles= false;
                super.fill(255, 0);
            }
            depthSort= hints[ENABLE_DEPTH_SORT];
            hint(ENABLE_DEPTH_SORT);
	    gl.glFrontFace(GL.GL_CCW);
	    gl.glEnable(GL.GL_NORMALIZE);
	    gl.glDepthFunc(GL.GL_LEQUAL);
        }
	history.addAlgorithm(algorithm);
	algorithms.add(algorithm);
	activeRenderers++;
    }

    /**
     * if this is the last algorithm calling end(), render all geometry stacked up so far and clear history and
     * set back all state to normal Processing rendering
     */
    public void end(Algorithm algorithm){
        
	history.removeAlgorithm(algorithm);
        if(activeRenderers==1){
            render();
            if(!renderTriangles) noFill();
            if(!depthSort)hint(DISABLE_DEPTH_SORT);
	    algorithms.clear();
            history.clear();
	    textureUnit= 1;
	    gl.glFrontFace(GL.GL_CW);
	    gl.glDisable(GL.GL_NORMALIZE);
        }
	activeRenderers--;
    }

    public GL beginGL(){
	
	gl.glFrontFace(GL.GL_CW);
	return super.beginGL();
    }

    public void endGL(){
	
	super.endGL();
	if(npr()) gl.glFrontFace(GL.GL_CCW);
    }

    protected void render() {


	if(npr()){
	    renderNPR();
	}
	else{
	    if (triangleCount > 0) {
		renderTriangles(0, triangleCount);
		if (raw != null) {
		    rawTriangles(0, triangleCount);
		}
	    }
	}
	if (pointCount > 0) {
	    renderPoints(0, pointCount);
	    if (raw != null) {
		rawPoints(0, pointCount);
	    }
	    pointCount = 0;
	}
	if (lineCount > 0) {
	    renderLines(0, lineCount);
	    if (raw != null) {
		rawLines(0, lineCount);
	    }
	    lineCount = 0;
	    pathCount = 0;
	}
	triangleCount = 0;
    }


    protected void renderPoints(){
        renderPoints(0, pointCount);
        if (raw != null) {
            rawPoints(0, pointCount);
        }
        pointCount = 0;
    }


    protected void renderLines(){
        renderLines(0, lineCount);
        if (raw != null) {
            rawLines(0, lineCount);
        }
        lineCount = 0;
    }

    
    protected void renderNPR(){

        gl.glEnable (GL.GL_DEPTH_TEST);   
	gl.glDepthMask(true);
	
	// preprocess
	ListIterator<Algorithm> algorithmIterator= algorithms.listIterator();
	Algorithm algorithm;
	while(algorithmIterator.hasNext()){
	    algorithm= algorithmIterator.next();
	    algorithm.preProcess();
	}
	
	if(backFaceCulling){
	    gl.glEnable(GL.GL_CULL_FACE);
	    gl.glCullFace(GL.GL_BACK);
	}
	else{
	    gl.glDisable(GL.GL_CULL_FACE);
	}

	ListIterator<History.Event> eventIterator= history.algorithmHistory.listIterator();
	History.Event start= eventIterator.next(), stop;
	int j= 0;	
	//System.out.println("\nEvent " + j + ":\n" + start);
	
	while(eventIterator.hasNext()){

	    j++;

	    stop= eventIterator.next();
	    //System.out.println("\nEvent " + j + ":\n" + stop);
	    
	    main.startShader();

	    int location;

	    location= main.getUniformLocation("mapShadow");
	    main.setBoolean(location, false);

	    location= main.getUniformLocation("cel");
	    main.setBoolean(location, false);


	    // set shader texture units
	    location= main.getUniformLocation("lookUpTable");
	    main.setTextureUnit(location, 0);

	    for(int i= 0; i<MAX_LIGHTS-1; i++){
		location= main.getUniformLocation("shadowMaps[" + i + "]");
		main.setTextureUnit(location, i+1);
	    }
	    
	    //prepare
	    algorithmIterator= start.algorithms.listIterator();
	    while(algorithmIterator.hasNext()){
		algorithm= algorithmIterator.next();
		algorithm.prepare();
	    }
	    
	    // render
	    // modelview goes to GL_MODELVIEW, camera inverse to texture matrices for shadows
	    int[] modelviewTarget= {GL.GL_MODELVIEW};

	    render(history.getIndex(start), history.getIndex(stop), modelviewTarget, null, null, textureUnits);

	    main.endShader();

	    //clean up
	    algorithmIterator= start.algorithms.listIterator();
	    while(algorithmIterator.hasNext()){
		algorithm= algorithmIterator.next();
		algorithm.cleanUp();
	    }
	    start= stop;
	}

	// postprocess
	algorithmIterator= algorithms.listIterator();
	while(algorithmIterator.hasNext()){
	    algorithm= algorithmIterator.next();
	    algorithm.postProcess();
	}
	
        if(hints[DISABLE_DEPTH_TEST]){
	    gl.glDisable(GL.GL_DEPTH_TEST);
	    gl.glClear(GL.GL_DEPTH_BUFFER_BIT);
	}
	
	gl.glDisable(GL.GL_CULL_FACE);
	
	gl.glMatrixMode(GL.GL_TEXTURE);
	for(int i=0; i<textureUnits.length; i++){
	    gl.glActiveTexture(GL.GL_TEXTURE0+i);
	    gl.glLoadIdentity();
	}
	gl.glActiveTexture(GL.GL_TEXTURE0);
	gl.glMatrixMode(GL.GL_MODELVIEW);	      
    }


    public void render(int start, int stop){
	
	int[] modelviewTarget= {GL.GL_MODELVIEW};
	render(start, stop, modelviewTarget, null, null, null);
    }
	

    public void render(int start, int stop, int[] modelviewTargets, int[] modelTargets, int[] cameraTargets, int[] cameraInvTargets){

	History.Event event;
	for(int i= start; i<stop; i++){
	    event= history.get(i);

	    // push modelview matrix on specified target(s)
	    if(modelviewTargets!=null){
		for(int j= 0; j<modelviewTargets.length; j++){
		    if(isTextureUnit(modelviewTargets[j])){
			gl.glActiveTexture(modelviewTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(modelviewTargets[j]);
		    }
		    gl.glPushMatrix();
		    gl.glMultMatrixf(event.modelview, 0);
		}
	    }

	    // push model matrix on specified target(s)
	    if(modelTargets!=null){
		for(int j= 0; j<modelTargets.length; j++){
		    if(isTextureUnit(modelTargets[j])){
			gl.glActiveTexture(modelTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(modelTargets[j]);
		    }
		    gl.glPushMatrix();
		    gl.glMultMatrixf(event.model, 0);
		}
	    }

	    // push camera matrix on specified target(s)
	    if(cameraTargets!=null){
		for(int j= 0; j<cameraTargets.length; j++){
		    if(isTextureUnit(cameraTargets[j])){
			gl.glActiveTexture(cameraTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(cameraTargets[j]);
		    }
		    gl.glPushMatrix();
		    gl.glMultMatrixf(event.camera, 0);
		}
	    }

	    // push camera inverse matrix on specified target(s)
	    if(cameraInvTargets!=null){
		for(int j= 0; j<cameraInvTargets.length; j++){
		    if(isTextureUnit(cameraInvTargets[j])){
			gl.glActiveTexture(cameraInvTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(cameraInvTargets[j]);
		    }
		    gl.glPushMatrix();
		    gl.glMultMatrixf(event.cameraInv, 0);
		    gl.glScalef(1.0f, -1.0f, 1.0f);
		}
	    }

	    // render
	    if(event.hasModels()){
		History.Model model;
		ListIterator<History.Model> listIterator= event.models.listIterator();
		while(listIterator.hasNext()){
		    model= listIterator.next();
		    gl.glColor4f(model.red, model.green, model.blue, model.alpha);
		    gl.glCallList(model.model.displayList);
		}	
	    }		    
	    else{
		renderTriangles(event.triangleIndex, history.get(i+1).triangleIndex); 
	    }

	    // pop modelview matrix
	    if(modelviewTargets!=null){
		for(int j= 0; j<modelviewTargets.length; j++){
		    if(isTextureUnit(modelviewTargets[j])){
			gl.glActiveTexture(modelviewTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(modelviewTargets[j]);
		    }
		    gl.glPopMatrix();
		}
	    }

	    // pop model matrix
	    if(modelTargets!=null){
		for(int j= 0; j<modelTargets.length; j++){
		    if(isTextureUnit(modelTargets[j])){
			gl.glActiveTexture(modelTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(modelTargets[j]);
		    }
		    gl.glPopMatrix();
		}
	    }

	    // pop camera matrix
	    if(cameraTargets!=null){
		for(int j= 0; j<cameraTargets.length; j++){
		    if(isTextureUnit(cameraTargets[j])){
			gl.glActiveTexture(cameraTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(cameraTargets[j]);
		    }
		    gl.glPopMatrix();
		}
	    }

	    // pop camera inverse matrix
	    if(cameraInvTargets!=null){
		for(int j= 0; j<cameraInvTargets.length; j++){
		    if(isTextureUnit(cameraInvTargets[j])){
			gl.glActiveTexture(cameraInvTargets[j]);
			gl.glMatrixMode(GL.GL_TEXTURE);
		    }
		    else{
			gl.glMatrixMode(cameraInvTargets[j]);
		    }
		    gl.glPopMatrix();
		}
	    }
	}
    }

    protected boolean isTextureUnit(int textureUnit){
	
	for(int i= 0; i<textureUnits.length; i++){
	    if(textureUnits[i]==textureUnit) return true;
	}
	return false;
    }

    protected void setBoolean(String identifier, boolean value){
	    
	int location= main.getUniformLocation(identifier);
	main.setBoolean(location, value);
    }


    protected void setInteger(String identifier, int value){
	    
	int location= main.getUniformLocation(identifier);
	main.setInteger(location, value);
    }


    protected void setFloat(String identifier, float value){
	    
	int location= main.getUniformLocation(identifier);
	main.setFloat(location, value);
    }

   public void fullScreenQuad(float z){

        gl.glMatrixMode(GL.GL_PROJECTION); 
        gl.glPushMatrix(); 
        gl.glLoadIdentity ();
        gl.glMatrixMode(GL.GL_MODELVIEW);
        gl.glPushMatrix(); 
        gl.glLoadIdentity();
 
        gl.glBegin(GL.GL_QUADS); 
	gl.glNormal3f(0.0f, 0.0f, 1.0f);
	gl.glTexCoord2f (0.0f, 0.0f); 
	gl.glVertex3f(-1.0f, -1.0f, z);
	gl.glTexCoord2f (1.0f, 0.0f);
        gl.glVertex3f(1.0f, -1.0f, z);
	gl.glTexCoord2f (1.0f, 1.0f);
	gl.glVertex3f(1.0f, 1.0f, z);
	gl.glTexCoord2f (0.0f, 1.0f); 
        gl.glVertex3f(-1.0f, 1.0f, z);
        gl.glEnd(); 

	gl.glMatrixMode(GL.GL_PROJECTION);
        gl.glPopMatrix(); 
        gl.glMatrixMode(GL.GL_MODELVIEW);
	gl.glPopMatrix(); 
    }

    public PMatrix3D getCameraInv(){

	return cameraInv;
    }

    public void setCameraInv(PMatrix3D cameraInv){
	
	this.cameraInv.set(cameraInv);
    }
}
